Python: add agent-framework-hosting core package#5638
Python: add agent-framework-hosting core package#5638eavanvalkenburg wants to merge 2 commits intomicrosoft:feature/python-hostingfrom
Conversation
There was a problem hiding this comment.
Automated Code Review
Reviewers: 3 | Confidence: 90%
✓ Correctness
This PR introduces a well-structured multi-channel hosting package for the Python Agent Framework. After verifying the key integration points against the core framework source (Workflow.run signatures, ResponseStream behavior, Message constructor, FileCheckpointStorage, RunerContext.has_checkpointing), I found no correctness bugs. The checkpoint handling, stream bridging, response delivery routing, and isolation middleware all use the framework APIs correctly. Edge cases (double-finalization in workflow streams, idempotent ExitStack close, session caching) are properly handled.
✓ Test Coverage
The new
agent-framework-hostingpackage introduces significant functionality in three modules (_host.py,_isolation.py,_types.py) with tests intest_host.pyandtest_types.py. Test coverage for host wiring, agent invocation, sessions, workflow targets, checkpointing, and delivery routing is solid. However, there are three notable gaps: (1)_isolation.pyhas zero dedicated tests — theIsolationKeysdataclass, the contextvar get/set/reset helpers, and critically the_FoundryIsolationASGIMiddlewarethat lifts HTTP headers into the contextvar are all untested; (2)apply_run_hook(exported utility for channels to invoke run hooks supporting both sync and async callables) has no test; (3)_wrap_inputis tested only for thestrinput path but theMessageandlist[Message]branches (which mutateadditional_propertieson existing objects) are not exercised.
✗ Design Approach
I found two design-level issues that warrant changes before this host API is safe to build channels on. First, the host reuses a single live
Workflowobject for every request, but core workflows explicitly preserve in-memory state acrossrun()calls, so new conversations can inherit prior conversation state whenever there is no per-conversation restore to overwrite it. Second,ResponseTarget.activeis resolved from_active, but the host updates_activeduringrun()/run_stream()before channels can calldeliver_response(), which makes the current inbound channel look active and breaks the intended cross-channel routing behavior.
Automated review by eavanvalkenburg's agents
129a1e2 to
0671a22
Compare
New ``agent-framework-hosting`` package implementing ADR 0026 / SPEC-002:
the channel-neutral host that lets a single ``Agent`` (or ``Workflow``)
fan out across multiple wire protocols ("channels") behind one Starlette
ASGI app.
Surface (re-exported from ``agent_framework_hosting``):
- ``AgentFrameworkHost`` — wraps a hostable target, mounts channels onto
an ASGI app, owns per-isolation-key ``AgentSession`` reuse, threads
request context (``response_id`` / ``previous_response_id``) into
context providers via an ``ExitStack`` of ``bind_request_context``
calls, and exposes an opt-in Hypercorn ``serve()`` helper (extra
``[serve]``).
- ``Channel`` protocol + ``ChannelContribution`` — the surface a channel
package implements (routes, lifespans, identity hooks, …).
- ``ChannelRequest`` / ``ChannelSession`` / ``ChannelIdentity`` /
``ChannelPush`` / ``ChannelCommand[Context]`` / ``ChannelRunHook`` /
``ChannelStreamTransformHook`` / ``DeliveryReport`` /
``HostedRunResult`` / ``ResponseTarget`` / ``ResponseTargetKind`` /
``apply_run_hook`` — channel-side dataclasses + helpers.
- ``IsolationKeys`` + ``ISOLATION_HEADER_USER`` / ``..._CHAT`` +
``get/set/reset_current_isolation_keys`` — the host's ASGI middleware
reads the ``x-agent-{user,chat}-isolation-key`` headers off each
inbound request and exposes them to the agent stack via a
``ContextVar`` so storage-side providers (e.g.
``FoundryHostedAgentHistoryProvider``) can apply per-tenant
partitioning without channels having to forward anything.
Includes 45 unit tests covering the host, channel contributions,
isolation contextvar, and shared types. Registers the package in
``python/pyproject.toml`` ``[tool.uv.sources]`` and adds the matching
pyright ``executionEnvironments`` entry for tests.
Hypercorn is an optional dependency (``[serve]`` extra); the soft import
in ``serve()`` is annotated for pyright since it isn't on the default
install.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
0671a22 to
46d4c9c
Compare
Source-code changes - _suppress_already_consumed: narrow contract — RuntimeError now logs at WARNING with exc_info; non-RuntimeError still logs at exception(). Docstring clarifies that any non-clean teardown is observable. - _BoundResponseStream: add aclose() and route __await__ through get_final_response() so the binding is always released — fixes contextvar leak when channels abandon the stream or use the await-the-stream convenience. - Lifespan: aggregate startup/shutdown callback errors; every callback runs, all failures are logged with their qualname, and the first error is re-raised so Starlette still aborts boot. - _build_run_kwargs: switch session-cache write to dict.setdefault so concurrent racers cannot orphan a session if create_session ever yields. - _deliver_response: introduce DeliveryReport.failed for push outages vs explicit "no link" drops; an outage no longer triggers an originating fallback so the channel can decide degraded behaviour. Test additions - tests/test_isolation.py (new): full coverage of IsolationKeys, the contextvar helpers, header constants, and end-to-end ASGI middleware lift / reset / passthrough. - tests/test_host.py: TestBindRequestContext, TestBoundResponseStream (aclose / __await__ / __getattr__ forwarding / double-close idempotency), TestWrapInputListMessages (list[Message] LAST precedence), TestLifespanAggregation (startup + shutdown). - tests/test_types.py: TestApplyRunHook (sync/async/None), and TestDeliveryReport (new failed field). - Updated test_push_exception_marks_skipped -> test_push_exception_lands_in_failed_no_fallback to match the new delivery contract. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| result = await workflow.run(request.input) | ||
| outputs = result.get_outputs() | ||
| text = "\n".join(_workflow_output_to_text(o) for o in outputs) if outputs else "" | ||
| return HostedRunResult(text=text) |
There was a problem hiding this comment.
Will it support other modalities in the future?
| workflow: Workflow = self.target # type: ignore[assignment] | ||
| storage = self._resolve_checkpoint_storage(request) | ||
|
|
||
| async def _maybe_restore() -> None: |
There was a problem hiding this comment.
The non streaming case doesn't seem to have this restore routine.
|
|
||
| routes.append(Route("/readiness", _readiness, methods=["GET"])) | ||
|
|
||
| for channel in self.channels: |
There was a problem hiding this comment.
What happens if the agent receives requests from multiple channels at the same time?
Motivation and Context
Implements §4 (Core abstractions) and §5 (
AgentFrameworkHost) of SPEC-002 (merged via #5549). This is the foundation package every channel (Responses, Invocations, Activity Protocol, Teams, Telegram, Entra link) imports from.Description
Adds the new
agent-framework-hostingpackage (python/packages/hosting/) with:Channelprotocol — the contract every channel package implements (name,contribute(), optionalon_init/on_closelifecycle hooks).AgentFrameworkHost— Starlette ASGI app that wires together a target (Agent,Workflow, or callable) with one or more channels, handles isolation-key threading, identity normalization, and run hooks per-channel.ChannelRequest/ChannelResponseenvelopes — the uniform structure each channel transforms its protocol into before invoking the target.ChannelSession+isolation_keyplumbing for per-conversation history bucketing.Workflowcan be the host's target alongsideAgent.tests/withtest_host.py,test_types.py, and shared fixtures.Workspace registration + lockfile entry so the package builds in CI.
Stack
PR-2 of 9 — the foundation. All channel PRs (PR-3..PR-7) and the samples PR (PR-8) depend on this landing first.
refactor/foundry-hosted-agent-history-providerfeat/hosting-corefeat/hosting-samplesContribution Checklist